Skip to content

fix: install script TTY handling on Unix#1032

Merged
vladfrangu merged 13 commits intomasterfrom
fix/install-script-on-unix
Mar 11, 2026
Merged

fix: install script TTY handling on Unix#1032
vladfrangu merged 13 commits intomasterfrom
fix/install-script-on-unix

Conversation

@vladfrangu
Copy link
Member

@vladfrangu vladfrangu commented Mar 2, 2026

Closes #1027

Summary

  • Fix the install shell script's TTY handling when running via curl | bash — shell-level </dev/tty redirects don't support raw mode properly for Node.js/Inquirer, so we now set APIFY_OPEN_TTY=1 and let Node.js open /dev/tty itself as a native tty.ReadStream
  • Bypass Inquirer entirely for the /dev/tty prompt path (Inquirer's internal readline cannot be closed, which hangs the process), using a lightweight single-keypress reader with proper cleanup
  • Skip adding PATH entries to shell config if already present
  • Add dev-test-install.sh for local testing of the install flow

Test plan

  • Run cat scripts/install/dev-test-install.sh | bash and verify the Y/n prompt works (accepts single keypress, clears line with answer)
  • Verify the process exits cleanly after the prompt (no extra keypress needed)
  • Run apify install directly in a terminal and verify the normal Inquirer prompt still works
  • Re-run the install and verify it skips adding PATH entries if already configured

🤖 Generated with Claude Code

@github-actions github-actions bot added this to the 135th sprint - Tooling team milestone Mar 2, 2026
@github-actions github-actions bot added the t-tooling Issues with this label are in the ownership of the tooling team. label Mar 2, 2026
@B4nan
Copy link
Member

B4nan commented Mar 5, 2026

Claude review from my end here, it sounds pretty fine, I would include it in v1.3, but please take a look at those points.

  Overall Assessment

  Good PR that solves a real UX problem — the curl | bash install flow was broken because stdin is consumed by the pipe, making Inquirer's prompt non-functional. The approach of signaling via APIFY_OPEN_TTY=1 and opening /dev/tty directly is the correct
  pattern for this.

  That said, I have several concerns ranging from a potential bug to design issues:

  ---
  Issues

  1. 🔴 Module-level process.env.HOME! — side effect at import time

  const defaultInstallDir = process.env.APIFY_CLI_INSTALL ?? join(process.env.HOME!, '.apify');
  const defaultBinDir = process.env.FINAL_BIN_DIR ?? join(defaultInstallDir, 'bin');

  These run at import time — and _register.ts imports InstallCommand unconditionally for every CLI invocation. If HOME is ever unset (e.g., in a container or CI), path.join(undefined, '.apify') will throw and crash the entire CLI, not just the install
  command.

  Fix: Move these inside promptAddToShell() as local variables, or make them lazy getters.

  2. 🔴 PR claims "skip adding PATH entries if already present" but there's no deduplication logic

  The description says:
  Skip adding PATH entries to shell config if already present

  But the code never checks oldContent for existing entries. The only guard is the install marker, which is existing behavior. If a user deletes .install-marker and re-runs, or if the config already has the entries from a manual addition, the lines will
  be appended again.

  Suggestion: Add a simple check:
  if (oldContent.includes('APIFY_CLI_INSTALL')) {
      info({ message: chalk.gray(`PATH entries already present in ${tildify(configFile)}`) });
  } else {
      // append
  }

  3. 🟡 File descriptor leak if new ReadStream(fd) throws

  In confirmFromTty:
  const fd = openSync('/dev/tty', 'r');
  ttyStream = new ReadStream(fd);

  If new ReadStream(fd) throws, fd is leaked — the finally block only destroys the stream (which was never assigned). The raw fd should be tracked separately and closed in the finally.

  } finally {
      if (ttyStream) {
          ttyStream.setRawMode(false);
          ttyStream.pause();
          ttyStream.destroy();
      } else if (fd !== undefined) {
          closeSync(fd);
      }
  }

  4. 🟡 readFile .catch(() => '') silently swallows permission errors

  const oldContent = await readFile(configFile, 'utf-8').catch(() => '');

  This is fine for ENOENT (file doesn't exist), but it also swallows EACCES and other errors. If the file exists but isn't readable, the code will silently overwrite it with just the new lines. Consider catching only ENOENT:

  const oldContent = await readFile(configFile, 'utf-8').catch((e) => {
      if (e.code === 'ENOENT') return '';
      throw e;
  });

  5. 🟡 The if (!installDir) guard is now dead code

  const installDir = process.env.PROVIDED_INSTALL_DIR ?? defaultInstallDir;

  if (!installDir) {
      warning({ message: chalk.gray(`Install directory not found, cannot add to shell`) });
      return;
  }

  Since defaultInstallDir is always a non-empty string (assuming HOME is set), installDir can never be falsy. The guard is now unreachable. Either remove it or restructure the fallback logic so the guard still serves a purpose.

  6. 🟢 Minor: allowedToAutomaticallyDo mutated for unknown shell

  default:
      configFile = '~/.bashrc';
      // But never automatically add to the file as we don't know the shell
      allowedToAutomaticallyDo = false;
      break;

  This is a good defensive change — ~/.bashrc is a literal tilde string, not a resolved path, so writeFile would fail or write to the wrong place. Worth a comment clarifying that configFile here is display-only.

  7. 🟢 dev-test-install.sh

  Fine to commit for developer convenience, but consider adding a note in the file or README that it's for local development only — it runs yarn and yarn build-bundles and assumes you're at the repo root. It could confuse someone who stumbles on it
  expecting a production install script.

  ---
  Shell script changes look correct

  The TTY detection in install.sh is the standard pattern:
  if ! [ -t 0 ] && [ -r /dev/tty ]; then
      APIFY_OPEN_TTY=1 ...

  Moving GITHUB/github_repo after the AVX2 block is a harmless reorder (no dependency on target).

  ---
  Summary

  The core fix (custom TTY keypress reader bypassing Inquirer) is sound and well-implemented. The main blockers are the module-level side effect (#1) and the missing deduplication logic that the PR claims to have (#2). The fd leak (#3) and overly broad
  catch (#4) are worth fixing but less critical.

@danpoletaev danpoletaev force-pushed the fix/install-script-on-unix branch from 2a74122 to ca9351f Compare March 6, 2026 21:54
@B4nan B4nan force-pushed the fix/install-script-on-unix branch from ca9351f to 2a74122 Compare March 6, 2026 22:53
@B4nan B4nan requested a review from l2ysho March 10, 2026 16:28
Copy link
Contributor

@l2ysho l2ysho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vladfrangu This is legit, I think only 1. and 2. form @B4nan review are worth to take a look.

also I am testing locally in docker (cat scripts/install/dev-test-install.sh | bash) and I am not sure what should be actual outcome if Y.
Should I manually run bash or you were able to bypass this and apify should be available right after install?

@vladfrangu
Copy link
Member Author

Well, even if you type y in the prompt, it should tell you you may need to run source xxx, did it not do that?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the Apify CLI bundled installer flow on Unix when executed via curl | bash, by avoiding Inquirer hangs and improving PATH/shell integration.

Changes:

  • Add APIFY_OPEN_TTY-based /dev/tty prompting path that bypasses Inquirer and supports raw-mode single-keypress input.
  • Add shell detection + shell config file resolution helpers, and skip shell integration if apify/actor are already on PATH.
  • Update bundle build script to use Bun’s build() API and force-bundle proxy-agent; add a local dev install test script.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/lib/utils.ts Adds shell detection + shell config path resolution; adds debug logging for login client errors.
src/commands/cli-management/install.ts Adds /dev/tty confirm prompt, changes shell integration flow, and improves install output.
scripts/install/install.sh Sets APIFY_OPEN_TTY=1 when stdin isn’t a TTY so Node/Bun can open /dev/tty directly.
scripts/install/dev-test-install.sh Adds a local dev script to test the installer flow (including `curl
scripts/build-cli-bundles.ts Switches to Bun build() API, injects proxy-agent, and adjusts target lists/output generation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vladfrangu vladfrangu merged commit f149310 into master Mar 11, 2026
22 checks passed
@vladfrangu vladfrangu deleted the fix/install-script-on-unix branch March 11, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-tooling Issues with this label are in the ownership of the tooling team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Installation using scripts requires fiddling with env vars

4 participants